คู่มือที่ครอบคลุมเกี่ยวกับการทำความเข้าใจและใช้งานอัลกอริทึมการบีบอัดวิดีโอตั้งแต่เริ่มต้นโดยใช้ Python เรียนรู้ทฤษฎีและการปฏิบัติของ Video Codec สมัยใหม่
การสร้าง Video Codec ใน Python: เจาะลึกอัลกอริทึมการบีบอัด
ในโลกที่เชื่อมต่อกันอย่างรวดเร็ว วิดีโอคือราชา ตั้งแต่บริการสตรีมมิ่งและการประชุมทางวิดีโอ ไปจนถึงฟีดโซเชียลมีเดีย วิดีโอดิจิทัลครองการรับส่งข้อมูลทางอินเทอร์เน็ต แต่เป็นไปได้อย่างไรที่จะส่งภาพยนตร์ความละเอียดสูงผ่านการเชื่อมต่ออินเทอร์เน็ตมาตรฐาน คำตอบอยู่ในสาขาที่น่าสนใจและซับซ้อน: การบีบอัดวิดีโอ หัวใจสำคัญของเทคโนโลยีนี้คือ video codec (COder-DECoder) ซึ่งเป็นชุดอัลกอริทึมที่ซับซ้อนซึ่งออกแบบมาเพื่อลดขนาดไฟล์ลงอย่างมากในขณะที่ยังคงคุณภาพของภาพไว้
แม้ว่า codecs มาตรฐานอุตสาหกรรมเช่น H.264, HEVC (H.265) และ AV1 ที่ไม่มีค่าลิขสิทธิ์จะเป็นงานวิศวกรรมที่ซับซ้อนอย่างไม่น่าเชื่อ แต่การทำความเข้าใจหลักการพื้นฐานของพวกเขาสามารถเข้าถึงได้สำหรับนักพัฒนาที่มีแรงจูงใจทุกคน คู่มือนี้จะนำคุณไปสู่การเดินทางสู่โลกแห่งการบีบอัดวิดีโอ เราจะไม่เพียงแค่พูดคุยเกี่ยวกับทฤษฎีเท่านั้น เราจะสร้าง video codec แบบง่าย ๆ เพื่อการศึกษาตั้งแต่เริ่มต้นโดยใช้ Python วิธีการลงมือปฏิบัติจริงนี้เป็นวิธีที่ดีที่สุดในการจับใจแนวคิดที่สง่างามที่ทำให้การสตรีมวิดีโอที่ทันสมัยเป็นไปได้
ทำไมต้อง Python? แม้ว่าจะไม่ใช่ภาษาที่คุณจะใช้สำหรับ codec เชิงพาณิชย์แบบเรียลไทม์ประสิทธิภาพสูง (ซึ่งโดยทั่วไปเขียนด้วย C/C++ หรือแม้แต่ assembly) แต่ความสามารถในการอ่านของ Python และไลบรารีที่มีประสิทธิภาพเช่น NumPy, SciPy และ OpenCV ทำให้เป็นสภาพแวดล้อมที่สมบูรณ์แบบสำหรับการเรียนรู้ สร้างต้นแบบ และการวิจัย คุณสามารถมุ่งเน้นไปที่อัลกอริทึมโดยไม่ต้องจมอยู่กับการจัดการหน่วยความจำระดับต่ำ
ทำความเข้าใจแนวคิดหลักของการบีบอัดวิดีโอ
ก่อนที่เราจะเขียนโค้ดแม้แต่บรรทัดเดียว เราต้องเข้าใจสิ่งที่เราพยายามทำให้สำเร็จ เป้าหมายของการบีบอัดวิดีโอคือการกำจัดข้อมูลที่ซ้ำซ้อน วิดีโอ RAW ที่ไม่ได้บีบอัดมีขนาดใหญ่มาก วิดีโอ 1080p เพียงหนึ่งนาทีที่ 30 เฟรมต่อวินาทีสามารถเกิน 7 GB ได้ เพื่อควบคุมสัตว์ร้ายข้อมูลนี้ เราใช้ประโยชน์จากความซ้ำซ้อนสองประเภทหลัก
เสาหลักสองประการของการบีบอัด: ความซ้ำซ้อนเชิงพื้นที่และเชิงเวลา
- ความซ้ำซ้อนเชิงพื้นที่ (ภายในเฟรม): นี่คือความซ้ำซ้อนภายในเฟรมเดียว ลองนึกถึงท้องฟ้าสีครามผืนใหญ่หรือผนังสีขาว แทนที่จะจัดเก็บค่าสีสำหรับทุกพิกเซลในพื้นที่นั้น เราสามารถอธิบายได้อย่างมีประสิทธิภาพมากขึ้น นี่คือหลักการเดียวกับรูปแบบการบีบอัดภาพเช่น JPEG
- ความซ้ำซ้อนเชิงเวลา (ระหว่างเฟรม): นี่คือความซ้ำซ้อนระหว่างเฟรมต่อเนื่องกัน ในวิดีโอส่วนใหญ่ ฉากไม่ได้เปลี่ยนไปอย่างสมบูรณ์จากเฟรมหนึ่งไปยังเฟรมถัดไป ตัวอย่างเช่น คนที่กำลังพูดคุยกับพื้นหลังแบบคงที่ มีความซ้ำซ้อนเชิงเวลาจำนวนมาก พื้นหลังยังคงเหมือนเดิม มีเพียงส่วนเล็ก ๆ ของภาพ (ใบหน้าและร่างกายของบุคคล) เท่านั้นที่เคลื่อนไหว นี่คือแหล่งที่มาของการบีบอัดที่สำคัญที่สุดในวิดีโอ
ประเภทเฟรมหลัก: I-frames, P-frames และ B-frames
เพื่อใช้ประโยชน์จากความซ้ำซ้อนเชิงเวลา ตัวแปลงสัญญาณจะไม่ปฏิบัติต่อทุกเฟรมอย่างเท่าเทียมกัน พวกเขาจัดหมวดหมู่เป็นประเภทต่าง ๆ โดยสร้างลำดับที่เรียกว่า Group of Pictures (GOP)
- I-frame (Intra-coded Frame): I-frame เป็นภาพที่สมบูรณ์และมีอยู่ในตัวเอง มันถูกบีบอัดโดยใช้ความซ้ำซ้อนเชิงพื้นที่เท่านั้น เหมือนกับ JPEG I-frames ทำหน้าที่เป็นจุดยึดในสตรีมวิดีโอ ทำให้ผู้ชมสามารถเริ่มเล่นหรือค้นหาตำแหน่งใหม่ได้ เป็นประเภทเฟรมที่ใหญ่ที่สุด แต่จำเป็นสำหรับการสร้างวิดีโอใหม่
- P-frame (Predicted Frame): P-frame ถูกเข้ารหัสโดยดูที่ I-frame หรือ P-frame ก่อนหน้า แทนที่จะจัดเก็บภาพทั้งหมด จะจัดเก็บเฉพาะความแตกต่างเท่านั้น ตัวอย่างเช่น จะจัดเก็บคำแนะนำเช่น "ใช้บล็อกพิกเซลนี้จากเฟรมสุดท้าย เลื่อนไปทางขวา 5 พิกเซล และนี่คือการเปลี่ยนแปลงสีเล็กน้อย" สิ่งนี้ทำได้ผ่านกระบวนการที่เรียกว่า การประมาณการเคลื่อนไหว
- B-frame (Bi-directionally Predicted Frame): B-frame มีประสิทธิภาพมากที่สุด สามารถใช้ทั้งเฟรมก่อนหน้าและเฟรมถัดไปเป็นข้อมูลอ้างอิงสำหรับการคาดการณ์ สิ่งนี้มีประโยชน์สำหรับฉากที่วัตถุถูกซ่อนไว้ชั่วคราวแล้วปรากฏขึ้นอีกครั้ง โดยการมองไปข้างหน้าและข้างหลัง ตัวแปลงสัญญาณสามารถสร้างการคาดการณ์ที่แม่นยำและมีประสิทธิภาพด้านข้อมูลมากขึ้น อย่างไรก็ตาม การใช้เฟรมในอนาคตจะทำให้เกิดความล่าช้าเล็กน้อย (เวลาแฝง) ทำให้ไม่เหมาะสำหรับการใช้งานแบบเรียลไทม์ เช่น วิดีโอคอล
GOP ทั่วไปอาจมีลักษณะเช่นนี้: I B B P B B P B B I ... ตัวเข้ารหัสจะตัดสินใจรูปแบบเฟรมที่ดีที่สุดเพื่อสร้างสมดุลระหว่างประสิทธิภาพการบีบอัดและความสามารถในการค้นหา
ขั้นตอนการบีบอัด: การแบ่งรายละเอียดทีละขั้นตอน
การเข้ารหัสวิดีโอสมัยใหม่เป็นไปป์ไลน์หลายขั้นตอน แต่ละขั้นตอนจะแปลงข้อมูลเพื่อให้บีบอัดได้มากขึ้น มาดูขั้นตอนสำคัญในการเข้ารหัสเฟรมเดียว

ขั้นตอนที่ 1: การแปลงปริภูมิสี (RGB เป็น YCbCr)
วิดีโอส่วนใหญ่อยู่ในปริภูมิสี RGB (แดง เขียว น้ำเงิน) อย่างไรก็ตาม ดวงตามนุษย์มีความไวต่อการเปลี่ยนแปลงความสว่าง (luma) มากกว่าการเปลี่ยนแปลงสี (chroma) ตัวแปลงสัญญาณใช้ประโยชน์จากสิ่งนี้โดยการแปลง RGB เป็นรูปแบบ luma/chroma เช่น YCbCr
- Y: องค์ประกอบ luma (ความสว่าง)
- Cb: องค์ประกอบ chroma ที่มีความแตกต่างของสีน้ำเงิน
- Cr: องค์ประกอบ chroma ที่มีความแตกต่างของสีแดง
โดยการแยกความสว่างออกจากสี เราสามารถใช้ การสุ่มตัวอย่างสี เทคนิคนี้ช่วยลดความละเอียดของช่องสี (Cb และ Cr) ในขณะที่รักษาความละเอียดเต็มที่สำหรับช่องความสว่าง (Y) ซึ่งดวงตาของเราไวต่อมากที่สุด รูปแบบทั่วไปคือ 4:2:0 ซึ่งทิ้งข้อมูลสี 75% โดยแทบไม่มีการสูญเสียคุณภาพที่รับรู้ได้ ทำให้ได้การบีบอัดทันที
ขั้นตอนที่ 2: การแบ่งพาร์ติชันเฟรม (Macroblocks)
ตัวเข้ารหัสไม่ได้ประมวลผลทั้งเฟรมในคราวเดียว มันแบ่งเฟรมออกเป็นบล็อกเล็กลง โดยทั่วไปคือ 16x16 หรือ 8x8 พิกเซล ซึ่งเรียกว่า macroblocks ขั้นตอนการประมวลผลที่ตามมาทั้งหมด (การคาดการณ์ การแปลง ฯลฯ) จะดำเนินการทีละบล็อก
ขั้นตอนที่ 3: การคาดการณ์ (Inter and Intra)
นี่คือจุดที่เวทมนตร์เกิดขึ้น สำหรับแต่ละ macroblock ตัวเข้ารหัสจะตัดสินใจว่าจะใช้การคาดการณ์ภายในเฟรมหรือระหว่างเฟรม
- สำหรับ I-frame (การคาดการณ์ภายใน): ตัวเข้ารหัสจะคาดการณ์บล็อกปัจจุบันโดยอิงจากพิกเซลของเพื่อนบ้านที่เข้ารหัสแล้ว (บล็อกด้านบนและด้านซ้าย) ภายในเฟรมเดียวกัน จากนั้นจะต้องเข้ารหัสความแตกต่างเล็กน้อย (ส่วนที่เหลือ) ระหว่างการคาดการณ์กับบล็อกจริงเท่านั้น
- สำหรับ P-frame หรือ B-frame (การคาดการณ์ระหว่าง): นี่คือ การประมาณการเคลื่อนไหว ตัวเข้ารหัสจะค้นหาบล็อกที่ตรงกันในเฟรมอ้างอิง เมื่อพบการจับคู่ที่ดีที่สุด จะบันทึก เวกเตอร์การเคลื่อนที่ (เช่น "เลื่อนไปทางขวา 10 พิกเซล ลง 2 พิกเซล") และคำนวณส่วนที่เหลือ บ่อยครั้งที่ส่วนที่เหลือใกล้เคียงกับศูนย์ ซึ่งต้องใช้บิตน้อยมากในการเข้ารหัส
ขั้นตอนที่ 4: การแปลง (เช่น Discrete Cosine Transform - DCT)
หลังจากการคาดการณ์ เรามีบล็อกส่วนที่เหลือ บล็อกนี้ถูกเรียกใช้ผ่านการแปลงทางคณิตศาสตร์เช่น Discrete Cosine Transform (DCT) DCT ไม่ได้บีบอัดข้อมูลด้วยตัวเอง แต่จะเปลี่ยนวิธีการแสดงอย่างสิ้นเชิง มันแปลงค่าพิกเซลเชิงพื้นที่เป็นสัมประสิทธิ์ความถี่ ความมหัศจรรย์ของ DCT คือสำหรับภาพธรรมชาติส่วนใหญ่ จะรวมพลังงานภาพส่วนใหญ่อยู่ในสัมประสิทธิ์เพียงไม่กี่ตัวที่มุมบนซ้ายของบล็อก (ส่วนประกอบความถี่ต่ำ) ในขณะที่สัมประสิทธิ์ที่เหลือ (สัญญาณรบกวนความถี่สูง) ใกล้เคียงกับศูนย์
ขั้นตอนที่ 5: Quantization
นี่คือขั้นตอน lossy หลักในไปป์ไลน์และเป็นกุญแจสำคัญในการควบคุมการแลกเปลี่ยนคุณภาพเทียบกับบิตเรต บล็อกสัมประสิทธิ์ DCT ที่แปลงแล้วจะถูกหารด้วยเมทริกซ์ quantization และผลลัพธ์จะถูกปัดเศษเป็นจำนวนเต็มที่ใกล้เคียงที่สุด เมทริกซ์ quantization มีค่าที่มากขึ้นสำหรับสัมประสิทธิ์ความถี่สูง ซึ่งจะบีบอัดหลาย ๆ ตัวให้เป็นศูนย์อย่างมีประสิทธิภาพ นี่คือจุดที่ข้อมูลจำนวนมากถูกทิ้งไป พารามิเตอร์ quantization ที่สูงขึ้นนำไปสู่ศูนย์มากขึ้น การบีบอัดที่สูงขึ้น และคุณภาพของภาพที่ต่ำลง (มักจะเห็นเป็นสิ่งประดิษฐ์ที่เป็นบล็อก)
ขั้นตอนที่ 6: Entropy Coding
ขั้นตอนสุดท้ายคือขั้นตอนการบีบอัดแบบไม่สูญเสียข้อมูล สัมประสิทธิ์ quantized, เวกเตอร์การเคลื่อนที่ และ metadata อื่น ๆ จะถูกสแกนและแปลงเป็นสตรีมไบนารี เทคนิคต่าง ๆ เช่น Run-Length Encoding (RLE) และ Huffman Coding หรือวิธีการขั้นสูงกว่าเช่น CABAC (Context-Adaptive Binary Arithmetic Coding) ถูกนำมาใช้ อัลกอริทึมเหล่านี้กำหนดรหัสที่สั้นกว่าให้กับสัญลักษณ์ที่พบบ่อยกว่า (เช่น ศูนย์จำนวนมากที่สร้างขึ้นโดย quantization) และรหัสที่ยาวกว่าให้กับสัญลักษณ์ที่พบน้อยกว่า ซึ่งบีบอัดบิตสุดท้ายออกจากสตรีมข้อมูล
ตัวถอดรหัสเพียงแค่ดำเนินการตามขั้นตอนเหล่านี้ในทางกลับกัน: Entropy Decoding -> Inverse Quantization -> Inverse Transform -> Motion Compensation -> Reconstructing the frame
การใช้งาน Video Codec แบบง่ายใน Python
ตอนนี้ มานำทฤษฎีไปปฏิบัติกัน เราจะสร้าง codec เพื่อการศึกษาที่ใช้ I-frames และ P-frames มันจะแสดงให้เห็นถึงไปป์ไลน์หลัก: Motion Estimation, DCT, Quantization และขั้นตอนการถอดรหัสที่สอดคล้องกัน
ข้อจำกัดความรับผิดชอบ: นี่คือ codec *ของเล่น* ที่ออกแบบมาเพื่อการเรียนรู้ มันไม่ได้ปรับให้เหมาะสมและจะไม่ให้ผลลัพธ์ที่เทียบได้กับ H.264 เป้าหมายของเราคือการเห็นอัลกอริทึมในการดำเนินการ
ข้อกำหนดเบื้องต้น
คุณจะต้องมีไลบรารี Python ต่อไปนี้ คุณสามารถติดตั้งได้โดยใช้ pip:
pip install numpy opencv-python scipy
โครงสร้างโปรเจ็กต์
มาจัดระเบียบโค้ดของเราเป็นไฟล์ไม่กี่ไฟล์:
main.py: สคริปต์หลักเพื่อเรียกใช้กระบวนการเข้ารหัสและถอดรหัสencoder.py: มีตรรกะสำหรับตัวเข้ารหัสdecoder.py: มีตรรกะสำหรับตัวถอดรหัสutils.py: ฟังก์ชันช่วยเหลือสำหรับวิดีโอ I/O และการแปลง
ส่วนที่ 1: ยูทิลิตี้หลัก (`utils.py`)
เราจะเริ่มต้นด้วยฟังก์ชันช่วยเหลือสำหรับ DCT, Quantization และ inverses ของพวกเขา นอกจากนี้เรายังต้องมีฟังก์ชันเพื่อแยกเฟรมออกเป็นบล็อก
# utils.py
import numpy as np
from scipy.fftpack import dct, idct
BLOCK_SIZE = 8
# A standard JPEG quantization matrix (scaled for our purposes)
QUANTIZATION_MATRIX = np.array([
[16, 11, 10, 16, 24, 40, 51, 61],
[12, 12, 14, 19, 26, 58, 60, 55],
[14, 13, 16, 24, 40, 57, 69, 56],
[14, 17, 22, 29, 51, 87, 80, 62],
[18, 22, 37, 56, 68, 109, 103, 77],
[24, 35, 55, 64, 81, 104, 113, 92],
[49, 64, 78, 87, 103, 121, 120, 101],
[72, 92, 95, 98, 112, 100, 103, 99]
])
def apply_dct(block):
"""Applies 2D DCT to a block."""
# Center the pixel values around 0
block = block - 128
return dct(dct(block.T, norm='ortho').T, norm='ortho')
def apply_idct(dct_block):
"""Applies 2D Inverse DCT to a block."""
block = idct(idct(dct_block.T, norm='ortho').T, norm='ortho')
# De-center and clip to valid pixel range
return np.round(block + 128).clip(0, 255)
def quantize(dct_block, qp=1):
"""Quantizes a DCT block. qp is a quality parameter."""
return np.round(dct_block / (QUANTIZATION_MATRIX * qp)).astype(int)
def dequantize(quantized_block, qp=1):
"""Dequantizes a block."""
return quantized_block * (QUANTIZATION_MATRIX * qp)
def frame_to_blocks(frame):
"""Splits a frame into 8x8 blocks."""
blocks = []
h, w = frame.shape
for i in range(0, h, BLOCK_SIZE):
for j in range(0, w, BLOCK_SIZE):
blocks.append(frame[i:i+BLOCK_SIZE, j:j+BLOCK_SIZE])
return blocks
def blocks_to_frame(blocks, h, w):
"""Reconstructs a frame from 8x8 blocks."""
frame = np.zeros((h, w), dtype=np.uint8)
k = 0
for i in range(0, h, BLOCK_SIZE):
for j in range(0, w, BLOCK_SIZE):
frame[i:i+BLOCK_SIZE, j:j+BLOCK_SIZE] = blocks[k]
k += 1
return frame
ส่วนที่ 2: ตัวเข้ารหัส (`encoder.py`)
ตัวเข้ารหัสเป็นส่วนที่ซับซ้อนที่สุด เราจะใช้งานอัลกอริทึมการจับคู่บล็อกแบบง่ายสำหรับการประมาณค่าการเคลื่อนไหว แล้วประมวลผล I-frames และ P-frames
# encoder.py
import numpy as np
from utils import apply_dct, quantize, frame_to_blocks, BLOCK_SIZE
def get_motion_vectors(current_frame, reference_frame, search_range=8):
"""A simple block matching algorithm for motion estimation."""
h, w = current_frame.shape
motion_vectors = []
for i in range(0, h, BLOCK_SIZE):
for j in range(0, w, BLOCK_SIZE):
current_block = current_frame[i:i+BLOCK_SIZE, j:j+BLOCK_SIZE]
best_match_sad = float('inf')
best_match_vector = (0, 0)
# Search in the reference frame
for y in range(-search_range, search_range + 1):
for x in range(-search_range, search_range + 1):
ref_i, ref_j = i + y, j + x
if 0 <= ref_i <= h - BLOCK_SIZE and 0 <= ref_j <= w - BLOCK_SIZE:
ref_block = reference_frame[ref_i:ref_i+BLOCK_SIZE, ref_j:ref_j+BLOCK_SIZE]
sad = np.sum(np.abs(current_block - ref_block))
if sad < best_match_sad:
best_match_sad = sad
best_match_vector = (y, x)
motion_vectors.append(best_match_vector)
return motion_vectors
def encode_iframe(frame, qp=1):
"""Encodes an I-frame."""
h, w = frame.shape
blocks = frame_to_blocks(frame)
quantized_blocks = []
for block in blocks:
dct_block = apply_dct(block.astype(float))
quantized_block = quantize(dct_block, qp)
quantized_blocks.append(quantized_block)
return {'type': 'I', 'h': h, 'w': w, 'data': quantized_blocks, 'qp': qp}
def encode_pframe(current_frame, reference_frame, qp=1):
"""Encodes a P-frame."""
h, w = current_frame.shape
motion_vectors = get_motion_vectors(current_frame, reference_frame)
quantized_residuals = []
k = 0
for i in range(0, h, BLOCK_SIZE):
for j in range(0, w, BLOCK_SIZE):
current_block = current_frame[i:i+BLOCK_SIZE, j:j+BLOCK_SIZE]
mv_y, mv_x = motion_vectors[k]
ref_block = reference_frame[i+mv_y : i+mv_y+BLOCK_SIZE, j+mv_x : j+mv_x+BLOCK_SIZE]
residual = current_block.astype(float) - ref_block.astype(float)
dct_residual = apply_dct(residual)
quantized_residual = quantize(dct_residual, qp)
quantized_residuals.append(quantized_residual)
k += 1
return {'type': 'P', 'motion_vectors': motion_vectors, 'data': quantized_residuals, 'qp': qp}
ส่วนที่ 3: ตัวถอดรหัส (`decoder.py`)
ตัวถอดรหัสจะย้อนกลับกระบวนการ สำหรับ P-frames จะทำการชดเชยการเคลื่อนไหวโดยใช้เวกเตอร์การเคลื่อนที่ที่เก็บไว้
# decoder.py
import numpy as np
from utils import apply_idct, dequantize, blocks_to_frame, BLOCK_SIZE
def decode_iframe(encoded_frame):
"""Decodes an I-frame."""
h, w = encoded_frame['h'], encoded_frame['w']
qp = encoded_frame['qp']
quantized_blocks = encoded_frame['data']
reconstructed_blocks = []
for q_block in quantized_blocks:
dct_block = dequantize(q_block, qp)
block = apply_idct(dct_block)
reconstructed_blocks.append(block.astype(np.uint8))
return blocks_to_frame(reconstructed_blocks, h, w)
def decode_pframe(encoded_frame, reference_frame):
"""Decodes a P-frame using its reference frame."""
h, w = reference_frame.shape
qp = encoded_frame['qp']
motion_vectors = encoded_frame['motion_vectors']
quantized_residuals = encoded_frame['data']
reconstructed_blocks = []
k = 0
for i in range(0, h, BLOCK_SIZE):
for j in range(0, w, BLOCK_SIZE):
# Decode the residual
dct_residual = dequantize(quantized_residuals[k], qp)
residual = apply_idct(dct_residual)
# Perform motion compensation
mv_y, mv_x = motion_vectors[k]
ref_block = reference_frame[i+mv_y : i+mv_y+BLOCK_SIZE, j+mv_x : j+mv_x+BLOCK_SIZE]
# Reconstruct the block
reconstructed_block = (ref_block.astype(float) + residual).clip(0, 255)
reconstructed_blocks.append(reconstructed_block.astype(np.uint8))
k += 1
return blocks_to_frame(reconstructed_blocks, h, w)
ส่วนที่ 4: การรวบรวมทุกอย่างเข้าด้วยกัน (`main.py`)
สคริปต์นี้จัดการกระบวนการทั้งหมด: การอ่านวิดีโอ การเข้ารหัสเฟรมต่อเฟรม แล้วจึงถอดรหัสเพื่อสร้างเอาต์พุตสุดท้าย
# main.py
import cv2
import pickle # For saving/loading our compressed data structure
from encoder import encode_iframe, encode_pframe
from decoder import decode_iframe, decode_pframe
def main(input_path, output_path, compressed_file_path):
cap = cv2.VideoCapture(input_path)
frames = []
while True:
ret, frame = cap.read()
if not ret:
break
# We'll work with the grayscale (luma) channel for simplicity
frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY))
cap.release()
# --- ENCODING --- #
print("Encoding...")
compressed_data = []
reference_frame = None
gop_size = 12 # I-frame every 12 frames
for i, frame in enumerate(frames):
if i % gop_size == 0:
# Encode as I-frame
encoded_frame = encode_iframe(frame, qp=2.5)
compressed_data.append(encoded_frame)
print(f"Encoded frame {i} as I-frame")
else:
# Encode as P-frame
encoded_frame = encode_pframe(frame, reference_frame, qp=2.5)
compressed_data.append(encoded_frame)
print(f"Encoded frame {i} as P-frame")
# The reference for the next P-frame needs to be the *reconstructed* last frame
if encoded_frame['type'] == 'I':
reference_frame = decode_iframe(encoded_frame)
else:
reference_frame = decode_pframe(encoded_frame, reference_frame)
with open(compressed_file_path, 'wb') as f:
pickle.dump(compressed_data, f)
print(f"Compressed data saved to {compressed_file_path}")
# --- DECODING --- #
print("\nDecoding...")
with open(compressed_file_path, 'rb') as f:
loaded_compressed_data = pickle.load(f)
decoded_frames = []
reference_frame = None
for i, encoded_frame in enumerate(loaded_compressed_data):
if encoded_frame['type'] == 'I':
decoded_frame = decode_iframe(encoded_frame)
print(f"Decoded frame {i} (I-frame)")
else:
decoded_frame = decode_pframe(encoded_frame, reference_frame)
print(f"Decoded frame {i} (P-frame)")
decoded_frames.append(decoded_frame)
reference_frame = decoded_frame
# --- WRITING OUTPUT VIDEO --- #
h, w = decoded_frames[0].shape
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(output_path, fourcc, 30.0, (w, h), isColor=False)
for frame in decoded_frames:
out.write(frame)
out.release()
print(f"Decoded video saved to {output_path}")
if __name__ == '__main__':
main('input.mp4', 'output.mp4', 'compressed.bin')
การวิเคราะห์ผลลัพธ์และการสำรวจเพิ่มเติม
หลังจากเรียกใช้สคริปต์ main.py ด้วยไฟล์ input.mp4 คุณจะได้รับไฟล์สองไฟล์: compressed.bin ซึ่งมีข้อมูลวิดีโอที่บีบอัดแบบกำหนดเองของเรา และ output.mp4 ซึ่งเป็นวิดีโอที่สร้างขึ้นใหม่ เปรียบเทียบขนาดของ input.mp4 กับ compressed.bin เพื่อดูอัตราส่วนการบีบอัด ตรวจสอบ output.mp4 ด้วยสายตาเพื่อดูคุณภาพ คุณอาจเห็นสิ่งประดิษฐ์ที่เป็นบล็อก โดยเฉพาะอย่างยิ่งกับค่า qp ที่สูงกว่า ซึ่งเป็นสัญญาณคลาสสิกของ quantization
การวัดคุณภาพ: Peak Signal-to-Noise Ratio (PSNR)
เมตริกวัตถุประสงค์ทั่วไปในการวัดคุณภาพของการสร้างใหม่คือ PSNR มันเปรียบเทียบเฟรมเดิมกับเฟรมที่ถอดรหัส PSNR ที่สูงขึ้นโดยทั่วไปบ่งชี้ถึงคุณภาพที่ดีขึ้น
import numpy as np
import math
def calculate_psnr(original, compressed):
mse = np.mean((original - compressed) ** 2)
if mse == 0:
return float('inf')
max_pixel = 255.0
psnr = 20 * math.log10(max_pixel / math.sqrt(mse))
return psnr
ข้อจำกัดและขั้นตอนต่อไป
codec แบบง่ายของเราเป็นการเริ่มต้นที่ดี แต่มันยังห่างไกลจากความสมบูรณ์แบบ นี่คือข้อจำกัดและการปรับปรุงที่อาจเกิดขึ้นซึ่งสะท้อนถึงวิวัฒนาการของ codecs ในโลกแห่งความเป็นจริง:
- การประมาณการเคลื่อนไหว: การค้นหาแบบละเอียดของเราช้าและพื้นฐาน Codecs จริงใช้ประโยชน์จากอัลกอริทึมการค้นหาแบบลำดับชั้นที่ซับซ้อนเพื่อค้นหาเวกเตอร์การเคลื่อนที่ได้เร็วกว่ามาก
- B-frames: เราใช้งานเฉพาะ P-frames การเพิ่ม B-frames จะช่วยปรับปรุงประสิทธิภาพการบีบอัดอย่างมากโดยมีค่าใช้จ่ายเพิ่มขึ้นในด้านความซับซ้อนและความหน่วง
- Entropy Coding: เราไม่ได้ใช้งานขั้นตอนการเข้ารหัส entropy ที่เหมาะสม เราเพียงแค่ pickled โครงสร้างข้อมูล Python การเพิ่ม Run-Length Encoder สำหรับศูนย์ quantized ตามด้วย Huffman หรือ Arithmetic coder จะช่วยลดขนาดไฟล์เพิ่มเติม
- Deblocking Filter: ขอบคมระหว่างบล็อก 8x8 ของเราทำให้เกิดสิ่งประดิษฐ์ที่มองเห็นได้ Codecs ที่ทันสมัยใช้ตัวกรอง deblocking หลังจากการสร้างใหม่เพื่อลดความคมชัดของขอบเหล่านี้และปรับปรุงคุณภาพของภาพ
- ขนาดบล็อกที่แปรผัน: Codecs ที่ทันสมัยไม่ได้ใช้เฉพาะ macroblocks ขนาด 16x16 ที่แก้ไขแล้วเท่านั้น พวกเขาสามารถแบ่งเฟรมออกเป็นขนาดและรูปร่างบล็อกที่หลากหลายเพื่อให้เข้ากับเนื้อหาได้ดีขึ้น (เช่น ใช้บล็อกขนาดใหญ่ขึ้นสำหรับพื้นที่เรียบและบล็อกขนาดเล็กลงสำหรับพื้นที่ที่มีรายละเอียด)
สรุป
การสร้าง video codec แม้จะเป็นแบบง่าย ๆ เป็นแบบฝึกหัดที่คุ้มค่าอย่างยิ่ง มันคลายความลึกลับของเทคโนโลยีที่ขับเคลื่อนส่วนสำคัญของชีวิตดิจิทัลของเรา เราได้เดินทางผ่านแนวคิดหลักของความซ้ำซ้อนเชิงพื้นที่และเชิงเวลา เดินผ่านขั้นตอนสำคัญของไปป์ไลน์การเข้ารหัส—การคาดการณ์ การแปลง และ quantization—และนำแนวคิดเหล่านี้ไปใช้ใน Python
โค้ดที่ให้ไว้ที่นี่เป็นจุดเริ่มต้น ฉันขอแนะนำให้คุณทดลองกับมัน ลองเปลี่ยนขนาดบล็อก พารามิเตอร์ quantization (`qp`) หรือความยาว GOP พยายามใช้งานรูปแบบ Run-Length Encoding แบบง่าย หรือแม้แต่เผชิญกับความท้าทายในการเพิ่ม B-frames โดยการสร้างและทำลายสิ่งต่าง ๆ คุณจะได้รับความซาบซึ้งอย่างสุดซึ้งต่อความเฉลียวฉลาดเบื้องหลังประสบการณ์วิดีโอที่ราบรื่นที่เรามักจะมองข้ามไป โลกแห่งการบีบอัดวิดีโอกว้างใหญ่และมีการพัฒนาอยู่ตลอดเวลา ซึ่งเปิดโอกาสมากมายสำหรับการเรียนรู้และนวัตกรรม